package org.codefilarete.stalactite.query.api;

import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.codefilarete.stalactite.query.model.From;
import org.codefilarete.stalactite.query.model.Query;
import org.codefilarete.stalactite.query.model.Union;
import org.codefilarete.tool.bean.Objects;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.KeepOrderSet;

/**
 * @author Guillaume Mary
 */
public interface QueryStatement extends SelectablesPod {
	
	/**
	 * Wraps current instance to make it appendable into a From clause
	 * @return an instance containing current instance and appendable into a From clause
	 */
	default PseudoTable asPseudoTable() {
		return asPseudoTable(null);
	}
	
	/**
	 * Wraps current instance to make it appendable into a From clause
	 * 
	 * @param name an optional name that serves as an alias of this instance into the From clause
	 * @return an instance containing current instance and appendable into a From clause
	 */
	default PseudoTable asPseudoTable(@Nullable String name) {
		return new PseudoTable(this, name);
	}
	
	/**
	 * Wrapper of {@link Query} and {@link Union} (as {@link QueryStatement}) to make them capable of being added to a {@link From} clause.
	 * 
	 * @author Guillaume Mary
	 */
	class PseudoTable implements Fromable {
		
		@Nullable
		private final String name;
		
		private final QueryStatement queryStatement;
		
		private final KeepOrderSet<PseudoColumn<Object>> columns = new KeepOrderSet<>();
		
		private final Map<Selectable<?>, String> aliases = new HashMap<>();
		
		public PseudoTable(QueryStatement queryStatement, @Nullable String name) {
			this.name = name;
			this.queryStatement = queryStatement;
			Map<Selectable<?>, String> unionAliases = queryStatement.getAliases();
			for (Selectable<?> column : queryStatement.getColumns()) {
				PseudoColumn<?> newPseudoColumn = new PseudoColumn<>(this, column.getExpression(), column.getJavaType());
				columns.add((PseudoColumn<Object>) newPseudoColumn);
				String alias = unionAliases.get(column);
				if (alias != null) {
					this.aliases.put(newPseudoColumn, alias);
				}
			}
		}
		
		public QueryStatement getQueryStatement() {
			return queryStatement;
		}
		
		@Nullable
		@Override
		public String getName() {
			return name;
		}
		
		@Override
		public String getAbsoluteName() {
			return getName();
		}
		
		@Override
		public Set<PseudoColumn<?>> getColumns() {
			return (Set) this.columns;
		}
		
		@Override
		public Map<Selectable<?>, String> getAliases() {
			return this.aliases;
		}
		
		/**
		 * @return union columns per name
		 */
		@Override
		public Map<String, PseudoColumn<?>> mapColumnsOnName() {
			return Iterables.map(getColumns(), objectPseudoColumn -> Objects.preventNull(aliases.get(objectPseudoColumn), objectPseudoColumn.getExpression()));
		}
	}
	
	class PseudoColumn<O> implements JoinLink<Fromable, O> {
		
		private final SelectablesPod owner;	// Union or Query
		
		private final String name;
		
		private final Class<O> javaType;
		
		
		public PseudoColumn(SelectablesPod owner, String name, Class<O> javaType) {
			this.owner = owner;
			this.name = name;
			this.javaType = javaType;
		}
		
		/**
		 * To be called only for pseudo-column in a {@link PseudoTable}, else would throw a {@link ClassCastException}
		 * @return {@link PseudoTable} owning this column
		 */
		@Override
		public Fromable getOwner() {
			return (Fromable) owner;
		}
		
		@Override
		public String getExpression() {
			return name;
		}
		
		@Override
		public Class<O> getJavaType() {
			return javaType;
		}
	}
}
